package kik.client;

import static com.calclab.emite.core.client.xmpp.stanzas.XmppURI.uri;

import java.util.ArrayList;
import java.util.Collection;

import kik.client.chat.ChatChange;
import kik.client.common.Change;
import kik.client.whiteboard.WhiteboardChange;

import com.calclab.emite.core.client.bosh.BoshSettings;
import com.calclab.emite.core.client.xmpp.session.Session;
import com.calclab.emite.core.client.xmpp.session.Session.State;
import com.calclab.emite.core.client.xmpp.stanzas.Message;
import com.calclab.emite.core.client.xmpp.stanzas.Presence;
import com.calclab.emite.core.client.xmpp.stanzas.Presence.Type;
import com.calclab.emite.im.client.roster.Roster;
import com.calclab.emite.im.client.roster.RosterItem;
import com.calclab.suco.client.Suco;
import com.calclab.suco.client.events.Event;
import com.calclab.suco.client.events.Listener;
import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.Window;
import com.google.gwt.xml.client.Document;
import com.google.gwt.xml.client.Element;
import com.google.gwt.xml.client.XMLParser;

/**
 * Klasa Connection jest odpowiedzialna za komunikację z serwerem xmpp. Jest
 * wykorzystywana wyłącznie przez klasę {@link ConferenceManager}. Wykorzystane
 * są biblioteki:
 * <ul>
 * <li><a href="http://code.google.com/p/emite/">emite</a>, na licencji GNU LGPL
 * - implementacja połączeń xmpp,</li>
 * <li><a href="http://code.google.com/p/suco/">suco</a>, GNU LGPL -
 * implementacja obserwatorów i zdarzeń.</li>
 * </ul>
 * <p>
 * Za pomocą metod onChange, onLoginChange i onContactChange należy
 * zarejestrować obserwatorów odpowiednich zmian w konferencji, w stanie
 * zalogowania i liście kontaktów.
 * 
 * @author Bartłomiej Kucharczyk
 */
public class Connection {
	private Roster roster;
	private Session session;
	private final Event<Session.State> onLoginChange = new Event<Session.State>(
			"kik:onLoginChange");
	private final Event<Presence> onContactChange = new Event<Presence>(
			"kik:onContactChange");
	private final Event<Change> onChange = new Event<Change>("kik:onChange");
	ConferenceManager confMan;

	/**
	 * Tworzony jest obiekt połączenia, poprzez który można się połączyć z
	 * serwerem zdefiniowanym przez zmieną server. Nie następuje połączenie z
	 * serwerem.
	 * 
	 * @param server
	 *            Nazwa serwera jabbera, do którego tworzone jest połączenie.
	 */
	public Connection(String server, ConferenceManager _confMan) {
		confMan = _confMan;

		Suco.get(com.calclab.emite.core.client.bosh.Connection.class)
				.setSettings(new BoshSettings("proxy", "localhost"));

		roster = Suco.get(Roster.class);

		roster.onRosterRetrieved(new Listener<Collection<RosterItem>>() {
			public void onEvent(final Collection<RosterItem> items) {
				ArrayList<String> rost = new ArrayList<String>();
				for (final RosterItem item : items) {
					rost.add(item.getJID().toString());
				}
				// GWT.log(rost.toString(), null);
				confMan.setRoster(rost);
			}
		});

		session = Suco.get(Session.class);

		session.onStateChanged(new Listener<Session.State>() {
			public void onEvent(final State state) {
				if (state == State.loggedIn) {
				    session.send(new Presence());
				    session.setReady();
				}
				onLoginChange.fire(state);
			}
		});

		session.onMessage(new Listener<Message>() {
			public void onEvent(final Message message) {
				GWT.log(message.toString(), null);
				handleMessage(message);
			}
		});

		session.onPresence(new Listener<Presence>() {
			public void onEvent(final Presence presence) {
				GWT.log("Presence received from " + presence.getFrom() + ": "
						+ presence.toString(), null);

				if (presence.getType() == Type.unavailable) {
					// ktoś stał się niedostępny, wiec trzeba go usunac z list
					onChange.fire(new MemberChange(presence.getFrom().getJID()
							.toString(), "exited", null));
				}

				onContactChange.fire(presence);
			}
		});

	}

	/**
	 * Zwraca login zalogowanego użytkownika, a dokładniej jego JID.
	 * 
	 * @return JID zalogowanego użytkownika.
	 */
	public String getLogin() {
		return session.getCurrentUser().getJID().toString();
	}

	/**
	 * Odbiera wiadomość i przetwarza.
	 * 
	 * @param message
	 *            Wiadomość przekazana przez metodę onMessage.
	 */
	private void handleMessage(Message message) {
		GWT.log("Message received from " + message.getFrom() + ":"
				+ message.getBody(), null);

		Document xml;
		try {
			xml = XMLParser.parse(message.getBody());
		} catch (Exception e) {
			return;
		}

		Element tmp = xml.getDocumentElement();
		if (tmp != null) {
			Change c = null;
			// niestety GWT nie ma serializacji i trzeba "ręcznie" to robić
			String t = tmp.getAttributeNode("type").getValue();
			if (t.compareTo("ChatChange") == 0)
				c = new ChatChange();
			else if (t.compareTo("WhiteboardChange") == 0) {
				c = new WhiteboardChange();
				GWT.log("Odebrano WhiteboardChange w Connection (" + t + ")",
						null);
			} else if (t.compareTo("NewConference") == 0) {
				// obsłużenie zaproszenia do nowej konferencji
				handleNewConference(message);
			} else if (t.compareTo("MemberChange") == 0) {
				// dodanie nowego członka do konferencji
				c = new MemberChange("", "", "");
			}

			if (c != null) {
				c.deserialize(message.getBody());
				c.setFromUser(message.getFrom().getJID().toString());
				c.setType(t);
				//GWT.log("Odpalam obiekt: " + t, null);
				onChange.fire(c);
			}
		}
	}

	/**
	 * Implementuje zachowanie w przypadku odebrania zaproszenia do nowej
	 * konferencji. Wyświetlany jest komunikat, czy użytkownik chce przyjąć
	 * zaproszenie. Następnie w zależności od decyzji użytkownika, tworzona jest
	 * nowa konferencja {@link NewConference NewConference} lub nie i na końcu
	 * wysyłany jest komunikat do nadawcy w postaci obiektu MemberChange, czy
	 * przyjęto udział w konferencji. Jeżeli tak, wysyłana jest akcja "joined",
	 * jeżeli odrzucona - "exited".
	 * 
	 * @param message
	 *            Odebrany obiekt.
	 * @see MemberChange
	 */
	private void handleNewConference(Message message) {
		String a; // tutaj zapiszemy akcje uzytkownika (czy sie zgodzil i
		// dolaczyl do konferencji, czy raczej odrzucil

		NewConference nc = new NewConference(null, "", "");
		nc.deserialize(message.getBody());

		if (Window.confirm("Do you accept invitation to\nconference from "
				+ message.getFromAsString() + "?")) {
			// uzytkownik zgodzil sie przyjac konferencje,
			// dlatego trzeba ja pokazac
			Conference conf = new Conference(nc.getFromID(), confMan);
			nc.getMembers().add(nc.getFromUser());
			conf.setMembers(nc.getMembers());

			confMan.onChange(conf);

			confMan.getConferencePanelUI().addConferenceUI(conf);

			// wysylanie zgody do uzytkownika, ze przyjmujemy zaproszenie
			a = "joined";
		} else {
			// trzeba tylko wyslac do zapraszajacego, ze zaproszenie odrzucone
			a = "exited";
		}

		confMan.sendTo(message.getFromAsString(), new MemberChange(getLogin(),
				a, nc.getFromID()));
	}

	/**
	 * Testowanie, czy użytkownik jest zalogowany do serwera xmpp.
	 * 
	 * @return true, jeżeli zalogowany; false, jeżeli niezalogowany.
	 */
	public boolean isLogged() {
		return session.isLoggedIn();
	}

	/**
	 * Połączenie z serwerem xmpp jako podany użytkownik. Nazwa użytkownika
	 * login powinna być w pełnym formacie: login@serwer, gdyż możliwe są
	 * połączenia z użytkownikami z innych serwerów xmpp.
	 * 
	 * @param login
	 *            Login użytkownika.
	 * @param pass
	 *            Hasło użytkownika.
	 */
	public void jabberConnect(String login, String pass) {
		session.login(uri(login), pass);

		session.onStateChanged(new Listener<State>() {
		    public void onEvent(final State state) {
			if (state == State.loggedIn) {
			    session.send(new Presence());
			    session.setReady();
			}
		    }
		});
	}

	/**
	 * Rozłączenie z serwerem xmpp.
	 */
	public void jabberDisconnect() {
		GWT.log(session.getState().toString(), null);
		session.logout();
	}

	/**
	 * Dodaje listener do listy obserwatorów zmian wirtualnej tablicy. W klasie
	 * {@link Change} zakapsułkowane są zmiany przesyłane między klientami,
	 * które zwracane są do obserwatora.
	 * 
	 * @param listener
	 *            Obserwator do dodania.
	 */
	public void onChange(Listener<Change> listener) {
		onChange.add(listener);
	}

	/**
	 * Usuwa listener z listy obserwatorów zmian wirtualnej tablicy.
	 * 
	 * @param listener
	 *            Obserwator do usunięcia.
	 */
	public void onChangeRemove(Listener<Change> listener) {
		onChange.remove(listener);
	}

	/**
	 * Dodaje listener do listy obserwatorów zmian kontaktów (znajomych). Klasa
	 * {@link Presence Presence} implementuje obiekty zmian statusów kontaktów.
	 * Obiekt typu Presence jest zwracany do obserwatora.
	 * 
	 * @param listener
	 *            Obserwator do dodania.
	 */
	public void onContactChange(Listener<Presence> listener) {
		onContactChange.add(listener);
	}

	/**
	 * Usuwa listener z listy obserwatorów zmian kontaktów (znajomych).
	 * 
	 * @param listener
	 *            Obserwator do usunięcia.
	 */
	public void onContactChangeRemove(Listener<Presence> listener) {
		onContactChange.remove(listener);
	}

	/**
	 * Dodaje listener do listy obserwatorów zmian stanu zalogowania
	 * (zalogowany/wylogowany/błąd autoryzacji). W klasie {@link Session.State}
	 * zdefiniowane są wszystkie możliwe stany zalogowania. Obiekt typu
	 * Session.State jest zwracany do obserwatora.
	 * 
	 * @param listener
	 *            Obserwator do dodania.
	 */
	public void onLoginChange(Listener<Session.State> listener) {
		onLoginChange.add(listener);
	}

	/**
	 * Usuwa listener z listy obserwatorów zmian stanu zalogowania.
	 * 
	 * @param listener
	 *            Obserwator do usunięcia.
	 */
	public void onLoginChangeRemove(Listener<Session.State> listener) {
		onLoginChange.remove(listener);
	}

	/**
	 * Wysyłanie informacji message do użytkownika user.
	 * 
	 * @param user
	 *            Pełna nazwa użytkownika, odbiorcy wiadomości.
	 * @param message
	 *            Wiadomość do przesłania.
	 */
	public void sendMsg(String user, String message) {
		session.send(new Message(message, uri(user)));
	}

}